#!/usr/bin/env python

'''
  MUDServ - A Multi-User Dungeon Server
  A Text-based Client
  
  Copyright (c) 2009, Ecl3ctic Software
  All rights reserved.
  
  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are met:

  * Redistributions of source code must retain the above copyright notice,
    this list of conditions and the following disclaimer.
  * Redistributions in binary form must reproduce the above copyright notice,
    this list of conditions and the following disclaimer in the documentation
    and/or other materials provided with the distribution.
  * Neither the name of Ecl3ctic Software nor the names of its contributors
    may be used to endorse or promote products derived from this software
    without specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  POSSIBILITY OF SUCH DAMAGE.
'''

import socket
import sys
import logging
import platform
import datetime
import simplejson
import threading
from getpass import getpass
from time import sleep
from clientlib import *

__version__ = '04-12-09'
__author__ = 'Ian Wagner and Michael Chest'
__credits__ = ['Ian Wagner', 'Michael Chest', 'Brian McBrayer', 'Dan Alexander',
               'Zak Richards', 'Reuben Deleon']
__license__ = 'BSD'
__status__ = 'Development'
__copyright__ = 'Copyright 2009 Ecl3ctic Software'

if platform.system() != 'Windows':
  import curses
else:
  import wcurses as curses

logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s %(levelname)s %(message)s',
                    filename='mudserv.log')

consoleCommands = ['tell', 'who', 'version', 'motd', 'setname', 'wall']
nMessages = 4     # Number of lines for the message buffer
username = ''
newMap = False      # Set when we need to load up a new map

KEY_ESCAPE = 0x1B
KEY_DEL = 0x7F

class SyncManager(threading.Thread):
  def __init__(self, sock, stdscr):
    threading.Thread.__init__(self)
    self.commandQueue = []
    self.sock = sock
    self.stdscr = stdscr
    self.killed = False
    self.players = []
    self.messages = []
    self.responses = []
    
  def getPlayers(self):
    '''
      Return the last list of players we got from the server
    '''
    return self.players
  
  def getNextResponse(self):
    '''
      Get the next server response received by an enqueued command that requested
      a response, blocking if necessary
    '''
    while len(self.responses) == 0:
      sleep(0.1)
    return self.responses.pop()
    
  def enqueue(self, cmd, getResponse=False):
    '''
      Adds cmd (a command to execute) to the command queue
    '''
    self.commandQueue.insert(0, (cmd, getResponse))
    
  def kill(self):
    self.killed = True
    
  def addMessage(self, message):
    self.messages.append(message)
    
  def cmdsInQueue(self):
    return len(self.commandQueue)
    
  def run(self):
    while not self.killed:
      while self.commandQueue != []:
        cmd = self.commandQueue.pop()
        result = eval(cmd[0])
        if cmd[1]:
          self.responses.insert(0, result)
        logging.info(cmd)
        
      result = executeCommand(sock, 'getnearbyplayers')
      if result[0] == 0:
        self.players = simplejson.loads(result[1])
        
      result = executeCommand(sock, 'getmessage')
      if result[0] == 0:
        self.messages.append(result[1])
        
      sleep(1 / 3.0)

print 'Welcome to the Ecl3ctic MUD Text Client v' + __version__ + '!'
  
# Setup a socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Get the host and port information
if len(sys.argv) < 2:
  host = 'localhost'
else:
  host = sys.argv[1]

if len(sys.argv) > 2:
  port = int(sys.argv[2])
else:
  port = 5656

sys.stdout.write('Connecting to ' + str(host) + ' on port ' + str(port) + '...')
sys.stdout.flush()

# Open a socket
sock.connect((host, port))

sys.stdout.write('\n\n')

error = ''
lastBoard = ''
showConsole = False
consoleInput = ''

# Get server info
print "This server is running version " + executeCommand(sock, 'version')[1]
print

# Get the user's info
try:
  sys.stdout.write('Username: ')
  sys.stdout.flush()
  username = raw_input()
  
  sys.stdout.flush()
  password = getpass()
except:
  # Clean up
  sock.send('/quit/')
  sock.close()

  # Raise the exception
  raise

status = executeCommand(sock, 'login', [username, password])
if status[0] == 0:    
  # Setup curses
  stdscr = curses.initscr()
  curses.noecho()
  curses.cbreak()
  
  stdscr.nodelay(1)
  stdscr.keypad(1)
  stdscr.leaveok(0)
  stdscr_y, stdscr_x = stdscr.getmaxyx()
  
  boardArea = curses.newpad(512, 512)
  boardArea.nodelay(1)
  boardArea.keypad(1)
  boardArea.leaveok(0)
  
  nextBoardUpdate = datetime.datetime.now()
  x = int(executeCommand(sock, 'getinfo', [username, 'pos_x'])[1])
  y = int(executeCommand(sock, 'getinfo', [username, 'pos_y'])[1])
  
  syncManager = SyncManager(sock, stdscr)
  syncManager.start()
  k = -1
  
  padX = max(0, x - (stdscr_x / 2))
  padY = max(0, y - (stdscr_y / 2))
  
  # Main loop
  while True:
    c = stdscr.getch()
    
    if c == ord('q') and not showConsole:
      break
    elif c == curses.KEY_UP and chr(boardArea.inch(y - 1, x) & 255) not in hardTiles:
      syncManager.enqueue("executeCommand(self.sock, 'move', ['n'])")
      y -= 1
      if y - padY < (stdscr_y / 2) and padY > 0:
        padY -= 1
    elif c == curses.KEY_DOWN and chr(boardArea.inch(y + 1, x) & 255) not in hardTiles:
      syncManager.enqueue("executeCommand(self.sock, 'move', ['s'])")
      y += 1
      if y - padY > (stdscr_y / 2) and padY < 512 - stdscr_y:
        padY += 1
    elif c == curses.KEY_RIGHT and chr(boardArea.inch(y, x + 1) & 255) not in hardTiles:
      syncManager.enqueue("executeCommand(self.sock, 'move', ['e'])")
      x += 1
      if x - padX > (stdscr_x / 2) and padX < 512 - stdscr_x:
        padX += 1
    elif c == curses.KEY_LEFT and chr(boardArea.inch(y, x - 1) & 255) not in hardTiles:
      syncManager.enqueue("executeCommand(self.sock, 'move', ['w'])")
      x -= 1
      if x - padX < (stdscr_x / 2) and padX > 0:
        padX -= 1
    elif c == ord('c') and not showConsole:
      showConsole = True
      cursorPos = 0
      consoleInput = ''
    elif c == KEY_ESCAPE:
      showConsole = False
      stdscr.addstr(stdscr_y - 1, 0, ' ' * (10 + len(consoleInput)))
    elif showConsole and (c == ord('\n') or c == curses.KEY_ENTER):
      showConsole = False
      stdscr.addstr(stdscr_y - 1, 0, ' ' * (10 + len(consoleInput)))
      
      # Parse the command
      if len(consoleInput) > 0 and consoleInput.split(' ')[0] in consoleCommands:
        command = consoleInput.split(' ')[0]
        
        args = []
        if len(consoleInput.split(' ')) > 1:
          args = consoleInput.split(' ')[1:]
        
        result = executeCommand(sock, command, args)
        if result[0] == 0 and len(result[1]) > 0:
          syncManager.addMessage(result[1])
        
    elif showConsole and (c == curses.KEY_BACKSPACE or c == KEY_DEL):
      consoleInput = consoleInput[:-1]
      stdscr.addstr(stdscr_y - 1, 9 + len(consoleInput), ' ')
    elif showConsole:
      try:
        consoleInput += chr(c)
      except:
        pass    # Ignore non-printing characters
    
    if x == 0 or y == 0 or x == len(lastBoard.split('\n')[0]) - 2 or y == len(lastBoard.split('\n')) - 2:
      newMap = True
    
    now = datetime.datetime.now()
    if now > nextBoardUpdate or newMap:
      nextBoardUpdate = now + datetime.timedelta(seconds=30.0)
      try:
        if newMap:
          syncManager.addMessage('Loading map...')
          status = '\n'.join('\n'.join(syncManager.messages[-nMessages:]).split('\n')[-nMessages:])      # Get the last 5 lines of messages
          stdscr.addstr(stdscr_y - nMessages - 1, 0, status + (' ' * (stdscr_x - len(status) - 1)))    # Draw the status message
          stdscr.move(stdscr_y - (nMessages - len(status.split('\n'))) - 2, len(status.split('\n')[-1]))
          stdscr.refresh()
          
        while syncManager.cmdsInQueue() > 0:
          sleep(0.1)
        syncManager.enqueue("executeCommand(sock, 'getasciiboard')", True)
        result = syncManager.getNextResponse()
        if result[0] == 0:
          lastBoard = result[1]
          if newMap:
            syncManager.addMessage('Load complete')
        else:
          nextBoardUpdate = now
      except:
        error = 'Error communicating with the server!'
        break
    
    if newMap:
      # Force a player update
      #syncManager.enqueue("executeCommand(sock, 'getnearbyplayers')", True)
      #result = syncManager.getNextResponse()
      #if result[0] == 0:
      #  players = simplejson.loads(result[1])
      #else:
      #  error = 'Error communicating with the server!'
      #  break
      
      # Update our coordinates
      syncManager.enqueue("executeCommand(sock, 'getinfo', [username, 'pos_x'])", True)
      result = syncManager.getNextResponse()
      if result[0] == 0:
        x = int(result[1])
        
      syncManager.enqueue("executeCommand(sock, 'getinfo', [username, 'pos_y'])", True)
      result = syncManager.getNextResponse()
      if result[0] == 0:
        y = int(result[1])
      
      # Adjust the map render
      padX = max(0, x - (stdscr_x / 2))
      padY = max(0, y - (stdscr_y / 2))
      
      newMap = False
    else:
      players = syncManager.getPlayers()
      
    board = lastBoard
    boardArea.addstr(0, 0, board)
    
    for player in players:
      posY = player['y']
      posX = player['x']
      avi = 'P'
      
      if player['username'] != username:
        boardArea.addch(posY, posX, ord(avi))
    
    boardArea.addch(y, x, ord('@'))              # Draw the player
    boardArea.refresh(padY,padX, 0,0, stdscr_y - nMessages - 2,stdscr_x - 1)
    
    status = '\n'.join('\n'.join(syncManager.messages[-nMessages:]).split('\n')[-nMessages:])      # Get the last 5 lines of messages
    stdscr.addstr(stdscr_y - nMessages - 1, 0, status + (' ' * (stdscr_x - len(status) - 1)))    # Draw the status message
    stdscr.move(stdscr_y - (nMessages - len(status.split('\n'))) - 2, len(status.split('\n')[-1]))
    
    if showConsole:
      stdscr.addstr(stdscr_y - 1, 0, 'Console: ' + consoleInput)
      stdscr.move(stdscr_y - 1, 9 + len(consoleInput))
    
    stdscr.refresh()
    
    sleep(1.0/60.0)
else:
  print 'Failed to login: You either entered the wrong password or you do not own this username.'

# Clean up
try:
  syncManager.kill()
except:
  pass

try:
  executeCommand(sock, 'quit')
except:
  pass
sock.close()

curses.nocbreak()
stdscr.keypad(0)
curses.echo()
curses.endwin()

if error:
  print error